@file:OptIn(ExperimentalTime::class)

package com.gitee.wsl.time

import com.gitee.wsl.ext.base.currentDateTime
import com.gitee.wsl.ext.base.currentTimeZone
import com.gitee.wsl.time.T.NANOSECONDS_PER_MICROSECOND
import com.gitee.wsl.time.T.NANOSECONDS_PER_MILLISECOND
import kotlinx.datetime.DateTimePeriod
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.DayOfWeek
import kotlin.time.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime
import kotlinx.datetime.Month
import kotlinx.datetime.TimeZone
import kotlinx.datetime.UtcOffset
import kotlinx.datetime.atTime
import kotlinx.datetime.isoDayNumber
import kotlinx.datetime.minus
import kotlinx.datetime.plus
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.microseconds
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit
import kotlin.time.ExperimentalTime
import com.gitee.wsl.ext.base.currentTime
import kotlinx.datetime.DatePeriod
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.number
import kotlin.math.ceil
import kotlin.time.Duration.Companion.seconds

/**
 * The earliest supported [DateTime], which can be used as a "far past" sentinel.
 */
val LocalDateTime.Companion.MIN: LocalDateTime
    get() = LocalDateTime( LocalDate.MIN, LocalTime.MIN)

/**
 * The latest supported [DateTime], which can be used as a "far future" sentinel.
 */
val LocalDateTime.Companion.MAX: LocalDateTime
    get() = LocalDateTime( LocalDate.MAX, LocalTime.MAX)

fun LocalDateTime.Companion.now(zone: TimeZone = TimeZone.currentSystemDefault()): LocalDateTime {
   return currentTime.toLocalDateTime(zone)
}



val LocalDateTime.Companion.NIL: LocalDateTime get() = LocalDateTime(1900,0,1,0,0,0)

@OptIn(ExperimentalTime::class)
val LocalDateTime.isToday: Boolean
    get() = date.isToday

val LocalDateTime.isYesterday: Boolean
    get() = date.isYesterday

val LocalDateTime.isTomorrow: Boolean
    get() = date.isTomorrow

val LocalDateTime.isFuture: Boolean
    get() = this > currentDateTime

val LocalDateTime.isPast: Boolean
    get() = this < currentDateTime

val LocalDateTime.millisecond: Int
    get() = nanosecond / 1_000_000

inline val LocalDateTime.Companion.tomorrow: LocalDateTime
    get() = LocalDate.tomorrow.atStartOfDay()

inline val LocalDateTime.Companion.yesterday: LocalDateTime
    get() = LocalDate.yesterday.atStartOfDay()

val LocalDateTime.next: LocalDateTime
    get() = date.next.atTime(time)

val LocalDateTime.previous: LocalDateTime
    get() = date.previous.atTime(time)

fun LocalDateTime.toUtcInstant() = toInstant(TimeZone.UTC)

val LocalDateTime.measureDate: LocalDate
    get() {
        val hourThreshold = 4
        val localDate = date
        return if (time.hour > hourThreshold) {
            localDate
        } else {
            localDate.minusDays(1)
        }
    }

/**
 * Convenience function for creating a [LocalDateTime] using a [Year].
 *
 * @see [LocalDateTime]
 */
@ExperimentalTime
fun LocalDateTime(
    year: Year,
    monthNumber: Int=1,
    dayOfMonth: Int=1,
    hour: Int=0,
    minute: Int=0,
    second: Int=0,
    nanosecond: Int=0
): LocalDateTime =
    LocalDateTime(
        year = year.value,
        monthNumber = monthNumber,
        dayOfMonth = dayOfMonth,
        hour = hour,
        minute = minute,
        second = second,
        nanosecond = nanosecond
    )

/**
 * Convenience function for creating a [LocalDateTime] using a [Year].
 *
 * @see [LocalDateTime]
 */
@ExperimentalTime
fun LocalDateTime(
    year: Year,
    month: Month = Month.JANUARY,
    dayOfMonth: Int=1,
    hour: Int=0,
    minute: Int=0,
    second: Int=0,
    nanosecond: Int=0
): LocalDateTime =
    LocalDateTime(
        year = year.value,
        month = month,
        dayOfMonth = dayOfMonth,
        hour = hour,
        minute = minute,
        second = second,
        nanosecond = nanosecond
    )


/**
 * Convenience function for creating a [LocalDateTime] using a [Year].
 *
 * @see [LocalDateTime]
 */
@ExperimentalTime
fun LocalDateTime(
    year: Int=1900,
    month: Int=1,
    dayOfMonth: Int=1,
    hour: Int=0,
    minute: Int=0,
    second: Int=0,
    nanosecond: Int=0
): LocalDateTime =
    LocalDateTime(
        year = year,
        month = Month(month),
        dayOfMonth = dayOfMonth,
        hour = hour,
        minute = minute,
        second = second,
        nanosecond = nanosecond
    )

/**
 * Returns a copy of this date-time with the values of any individual components replaced by the new values
 * specified.
 * @throws DateTimeException if the resulting date-time is invalid
 */
fun LocalDateTime.copy(
    date: LocalDate = this.date,
    time: LocalTime = this.time
) = LocalDateTime(date, time)

/**
 * Returns a copy of this date-time with the values of any individual components replaced by the new values
 * specified.
 * @throws DateTimeException if the resulting date-time is invalid
 */
fun LocalDateTime.copy(
    year: Int = this.year,
    dayOfYear: Int,
    hour: Int = this.hour,
    minute: Int = this.minute,
    second: Int = this.second,
    nanosecond: Int = this.nanosecond
) = LocalDateTime(date.copy(year, dayOfYear), time.copy(hour, minute, second, nanosecond))


@OptIn(ExperimentalTime::class)
fun LocalDateTime.copy(
    year: Int = this.year,
    month: Int = this.monthNumber,
    dayOfMonth: Int = this.dayOfMonth,
    hour: Int = this.hour,
    minute: Int = this.minute,
    second: Int = this.second,
    nanosecond: Int = this.nanosecond
) = LocalDateTime(
    year,
    month,
    min(Month(month).days(isLeapYear(year)), dayOfMonth),
    hour,
    minute,
    second,
    nanosecond
)

fun LocalDateTime.atTime(time: LocalTime): LocalDateTime {
    return LocalDateTime(
        year = this.year,
        month = this.month,
        dayOfMonth = this.dayOfMonth,
        hour = time.hour,
        minute = time.minute,
        second = 0,
        nanosecond = 0,
    )
}

fun LocalDateTime.atMinute(minute: Int): LocalDateTime {
    return LocalDateTime(
        year = this.year,
        month = this.month,
        dayOfMonth = this.dayOfMonth,
        hour = this.hour,
        minute = minute,
        second = 0,
        nanosecond = 0,
    )
}

fun LocalDateTime.atSecond(second: Int): LocalDateTime {
    return LocalDateTime(
        year = this.year,
        month = this.month,
        dayOfMonth = this.dayOfMonth,
        hour = this.hour,
        minute = this.minute,
        second = second,
        nanosecond = 0,
    )
}

operator fun LocalDateTime.plus(dateTimePeriod: DateTimePeriod): LocalDateTime {
   return toInstant(TimeZone.currentSystemDefault()).plus(dateTimePeriod, TimeZone.currentSystemDefault()).toLocalDateTime(TimeZone.currentSystemDefault())
}

operator fun LocalDateTime.plus(months: MonthSpan): LocalDateTime = this + months.toDateTimePeriod()

operator fun LocalDateTime.plus(dateTimes: DateTimeSpan): LocalDateTime = this + dateTimes.toDateTimePeriod()

operator fun LocalDateTime.minus(dateTimePeriod: DateTimePeriod): LocalDateTime {
   return this.toInstant(TimeZone.currentSystemDefault()).minus(dateTimePeriod, TimeZone.currentSystemDefault()).toLocalDateTime(TimeZone.currentSystemDefault())
}

//operator fun LocalDateTime.plus(times: TimeSpan): LocalDateTime = this + times.toDateTimePeriod()

operator fun LocalDateTime.minus(months: MonthSpan): LocalDateTime = this - months.toDateTimePeriod()

operator fun LocalDateTime.minus(dateTimes: DateTimeSpan): LocalDateTime = this - dateTimes.toDateTimePeriod()

operator fun LocalDateTime.minus(other:LocalDateTime) = (this.toInstant(currentTimeZone) - other.toInstant(currentTimeZone))

operator fun LocalDateTime.minus(other:LocalDate) = this - other.atStartOfDay()

operator fun LocalDateTime.minus(other: Duration) = (this.toInstant(currentTimeZone) - other).toLocalDateTime(currentTimeZone)

operator fun LocalDateTime.plus(other: Duration) = (this.toInstant(currentTimeZone) + other).toLocalDateTime(currentTimeZone)

fun LocalDateTime.Companion.create(
  year: Int,
  month: Int,
  day: Int,
  hour: Int,
  minute: Int,
  second: Int,
  millisecond: Int = 0
) = LocalDateTime(LocalDate(year,month,day), LocalTime(hour, minute, second,millisecond*1_000_000))

fun LocalDateTime.Companion.parseOrNull(isoString: String): LocalDateTime? =
    runCatching { parse(isoString) }.getOrNull()

fun Long.toLocalDateTime(): LocalDateTime =
    Instant.fromEpochMilliseconds(this).toLocalDateTime(TimeZone.currentSystemDefault())

val LocalDateTime.timestamp get() = toEpochMilliseconds()

fun LocalDateTime.toEpochMilliseconds() = toInstant(currentTimeZone).toEpochMilliseconds()

inline val LocalDateTime.atStartOfDay: LocalDateTime
    get() = date.atStartOfDay()

inline val LocalDateTime.atEndOfDay: LocalDateTime
    get() = date.atEndOfDay()


val LocalDateTime.atFirstDayOfYear: LocalDateTime get() = LocalDateTime(this.year,1,1,0,0,0)

val LocalDateTime.atLastDayOfYear: LocalDateTime get() = LocalDateTime(this.year,12,31,23,59,59)

val LocalDateTime.atFirstDayOfMonth: LocalDateTime get() = LocalDateTime(this.year,this.monthNumber,1,0,0,0)

val LocalDateTime.lastDayOfMonth: LocalDateTime get() = LocalDateTime(this.year,this.monthNumber,
    getLastDayOnMonth(year,monthNumber),23,59,59)

internal fun Int.monthLength(isLeapYear: Boolean): Int =
    when (this) {
        2 -> if (isLeapYear) 29 else 28
        4, 6, 9, 11 -> 30
        else -> 31
    }

private fun getLastDayOnMonth(year: Int,month:Int) = month.monthLength(isLeapYear(year))


fun LocalDateTime.startOfYear(): Instant {
    return LocalDate(year = year, monthNumber = 1, dayOfMonth = 1)
        .atTime(hour = 12, minute = 0) // Make it GMT-BST transition safe
        .toSystemDefaultTimeZoneInstant()
        .atStartOfDay()
}

fun LocalDateTime.endOfYear(): Instant {
    return LocalDate(year = year, monthNumber = 12, dayOfMonth = 31)
        .atTime(hour = 12, minute = 0) // Make it GMT-BST transition safe
        .toSystemDefaultTimeZoneInstant()
        .atEndOfDay()
}

fun LocalDateTime.startOfMonth(): Instant {
    return LocalDate(year = year, monthNumber = monthNumber, dayOfMonth = 1)
        .atTime(hour = 12, minute = 0) // Make it GMT-BST transition safe
        .toSystemDefaultTimeZoneInstant()
        .atStartOfDay()
}

fun LocalDateTime.endOfMonth(): Instant {
    return LocalDate(year = year, monthNumber = monthNumber, dayOfMonth = 1)
        .plus(1, DateTimeUnit.MONTH)
        .minus(1, DateTimeUnit.DAY)
        .atTime(hour = 12, minute = 0) // Make it GMT-BST transition safe
        .toSystemDefaultTimeZoneInstant()
        .atEndOfDay()
}

fun LocalDateTime.startOfWeek(): Instant {
    val dayOfWeek = date.dayOfWeek
    val daysSinceSunday = dayOfWeek.isoDayNumber
    return date
        .minus(value = daysSinceSunday - 1, unit = DateTimeUnit.DAY)
        .atTime(hour = 12, minute = 0) // Make it GMT-BST transition safe
        .toSystemDefaultTimeZoneInstant()
        .atStartOfDay()
}

fun LocalDateTime.endOfWeek(): Instant {
    val daysUntilSunday = DayOfWeek.SUNDAY.isoDayNumber - dayOfWeek.isoDayNumber
    return date
        .plus(daysUntilSunday, DateTimeUnit.DAY)
        .atTime(hour = 12, minute = 0) // Make it GMT-BST transition safe
        .toSystemDefaultTimeZoneInstant()
        .atEndOfDay()
}


/**
 * Returns a new LocalDateTime that has either the same (if [this] has the same day of week as [dayOfWeek]) or
 * the previous [dayOfWeek].
 */
fun LocalDateTime.withPreviousOrSameDayOfWeek(
    dayOfWeek: DayOfWeek,
    zone: TimeZone,
): LocalDateTime {
    val daysDiff = dayOfWeek.isoDayNumber - this.dayOfWeek.isoDayNumber
    if (daysDiff == 0) return this

    val daysToAdd = if (daysDiff >= 0) 7 - daysDiff else -daysDiff
    return minusDays(daysToAdd, zone)
}

/**
 * Returns a new LocalDateTime that is set to [DayOfWeek.MONDAY] and [00:00:00.000] of the week of [this].
 */
 fun LocalDateTime.asStartOfWeek(zone: TimeZone): LocalDateTime =
    withPreviousOrSameDayOfWeek(DayOfWeek.MONDAY, zone).asMidnight()

/**
 * Returns a new LocalDateTime that is set to the first day of month and [00:00:00.000] of the month of [this].
 */
 fun LocalDateTime.asStartOfMonth(): LocalDateTime = LocalDateTime(year, month, 1, 0, 0, 0, 0)

/**
 * Returns a new LocalDateTime that is set to the first day of year and [00:00:00.000] of the year of [this].
 */
 fun LocalDateTime.asStartOfYear(): LocalDateTime = LocalDateTime(year, Month.JANUARY, 1, 0, 0, 0, 0)

/**
 * Adds a given value to the [this], converting it to [Instant] for the addition.
 */
 fun LocalDateTime.plus(value: Int, unit: DateTimeUnit, zone: TimeZone): LocalDateTime =
    toInstant(zone).plus(value, unit, zone).toLocalDateTime(zone)

/**
 * Returns a new [LocalDateTime] with the given [day] of month and everything else of [this].
 */
 fun LocalDateTime.withDayOfMonth(day: Int): LocalDateTime =
    LocalDateTime(year, month, day, hour, minute, second, nanosecond)

/**
 * Adds a specified number of [days] to [this].
 */
 fun LocalDateTime.plusDays(days: Int, zone: TimeZone): LocalDateTime = plus(days, DateTimeUnit.DAY, zone)

/**
 * Subtracts a specified number of [days] from [this].
 */
 fun LocalDateTime.minusDays(days: Int, zone: TimeZone): LocalDateTime = plus(-days, DateTimeUnit.DAY, zone)

/**
 * Returns a new [LocalDateTime] with the given [month] and everything else of [this].
 */
 fun LocalDateTime.withMonth(month: Month): LocalDateTime =
    LocalDateTime(year, month, dayOfMonth, hour, minute, second, nanosecond)

/**
 * Adds a specified number of [months] to [this]. Returns a new [LocalDateTime]
 */
 fun LocalDateTime.plusMonths(months: Int, zone: TimeZone): LocalDateTime = plus(months, DateTimeUnit.MONTH, zone)

/**
 * Subtracts a specified number of [months] from [this]. Returns a new [LocalDateTime]
 */
 fun LocalDateTime.minusMonths(months: Int, zone: TimeZone): LocalDateTime =
    plus(-months, DateTimeUnit.MONTH, zone)


/**
 * Computes the length of the current month of [LocalDateTime] in days
 */
 val LocalDateTime.lengthOfMonth: Int get() = month.length(year)


/**
 * Returns tru if [this] is on the same day as [other]. Ignores time, but considers year and month.
 */
 fun LocalDateTime.onSameDay(other: LocalDateTime): Boolean = date == other.date

/**
 * Returns a new [LocalDateTime] that has the date adjusted to the date with the given **next** [dayOfWeek].
 * If the date is already on the given [dayOfWeek], returns a date a week later.
 */
 fun LocalDateTime.withNextDayOfWeek(
    dayOfWeek: DayOfWeek,
    zone: TimeZone,
): LocalDateTime {
    val daysDiff = this.dayOfWeek.isoDayNumber - dayOfWeek.isoDayNumber
    val daysToAdd = if (daysDiff >= 0) 7 - daysDiff else -daysDiff
    return plusDays(daysToAdd, zone)
}

/**
 * Returns a new [LocalDateTime] set to the midnight.
 */
 fun LocalDateTime.asMidnight(): LocalDateTime = LocalDateTime(year, month, dayOfMonth, 0, 0, 0, 0)

/**
 * Create a new [LocalDateTime] with the given [time] and [nanos]
 * Preserves [nanos] of previous time
 */
 fun LocalDateTime.withTime(time: LocalTime, nanos: Int = nanosecond): LocalDateTime =
    LocalDateTime(year, month, dayOfMonth, time.hour, time.minute, time.second, nanos)


private inline fun LocalDateTime.nextTimeBy(next: LocalTime.() -> LocalTime?): LocalDateTime {
    return time.next()?.let { copy(time = it) } ?: (date + 1.days).atStartOfDay
}


internal fun LocalDateTime.copyIfChanged(nanosecond: Int): LocalDateTime {
    return if (nanosecond == this.nanosecond) this else copy(nanosecond = nanosecond)
}

internal fun LocalDateTime.copyIfChanged(time: LocalTime): LocalDateTime {
    return if (time === this.time) {
        this
    } else {
        copy(time = time)
    }
}

internal fun LocalDateTime.copyIfChanged(date: LocalDate, time: LocalTime): LocalDateTime {
    return if (date === this.date && time === this.time) {
        this
    } else {
        copy(date = date, time = time)
    }
}

fun LocalDateTime.nextWholeHour(): LocalDateTime = nextTimeBy(LocalTime::nextWholeHourOrNull)
fun LocalDateTime.nextWholeMinute(): LocalDateTime = nextTimeBy(LocalTime::nextWholeMinuteOrNull)
fun LocalDateTime.nextWholeSecond(): LocalDateTime = nextTimeBy(LocalTime::nextWholeSecondOrNull)

internal fun LocalDateTime.previousWholeHour(): LocalDateTime {
    return if (minute > 0 || second > 0 || nanosecond > 0) {
        copy(minute = 0, second = 0, nanosecond = 0)
    } else {
        this
    }
}

internal fun LocalDateTime.previousWholeMinute(): LocalDateTime {
    return if (second > 0 || nanosecond > 0) {
        copy(second = 0, nanosecond = 0)
    } else {
        this
    }
}

internal fun LocalDateTime.previousWholeSecond(): LocalDateTime {
    return if (nanosecond > 0) {
        copy(nanosecond = 0)
    } else {
        this
    }
}

internal fun LocalDateTime.previousWholeMillisecond(): LocalDateTime {
    return copyIfChanged(nanosecond = nanosecond / NANOSECONDS_PER_MILLISECOND * NANOSECONDS_PER_MILLISECOND)
}

internal fun LocalDateTime.previousWholeMicrosecond(): LocalDateTime {
    return copyIfChanged(nanosecond = nanosecond / NANOSECONDS_PER_MICROSECOND * NANOSECONDS_PER_MICROSECOND)
}

internal fun LocalDateTime.roundedToNearestUnchecked(increment: Duration): LocalDateTime {
    val newTime = time.roundedToNearestUnchecked(increment)

    return when {
        newTime == time -> this
        newTime == LocalTime.MIDNIGHT && time >= LocalTime.NOON -> LocalDateTime((date + 1.days).date, newTime)
        else -> copy(time = newTime)
    }
}

/**
 * Returns this date-time, rounded to match the precision of a given [unit]. If the time is halfway between whole values
 * of the unit, it will be rounded up.
 */
fun LocalDateTime.roundedTo(unit: DurationUnit): LocalDateTime {
    return when (unit) {
        DurationUnit.DAYS -> if (hour >= 12) (date + 1.days).atStartOfDay else date.atStartOfDay()
        DurationUnit.HOURS -> if (minute >= 30) nextWholeHour() else previousWholeHour()
        DurationUnit.MINUTES -> if (second >= 30) nextWholeMinute() else previousWholeMinute()
        DurationUnit.SECONDS -> if (nanosecond >= 500_000_000) nextWholeSecond() else previousWholeSecond()
        DurationUnit.MILLISECONDS -> roundedToNearestUnchecked(1.milliseconds)
        DurationUnit.MICROSECONDS -> roundedToNearestUnchecked(1.microseconds)
        DurationUnit.NANOSECONDS -> this
        else -> this
    }
}


internal fun LocalDateTime.roundedUpToNearestUnchecked(increment: Duration): LocalDateTime {
    val newTime = time.roundedUpToNearestUnchecked(increment)

    return when {
        newTime == time -> this
        newTime == LocalTime.MIDNIGHT && time > LocalTime.MIDNIGHT -> LocalDateTime((date + 1.days).date, newTime)
        else -> copy(time = newTime)
    }
}


/**
 * Returns this date-time, rounded up to match the precision of a given [unit].
 *
 * The start of the next day will be returned when the resulting time is after midnight.
 */
fun LocalDateTime.roundedUpTo(unit: DurationUnit): LocalDateTime {
    return when (unit) {
        DurationUnit.DAYS -> if (time > LocalTime.MIDNIGHT) (date + 1.days).atStartOfDay else this
        DurationUnit.HOURS -> if (minute > 0 || second > 0 || nanosecond > 0) nextWholeHour() else this
        DurationUnit.MINUTES -> if (second > 0 || nanosecond > 0) nextWholeMinute() else this
        DurationUnit.SECONDS -> if (nanosecond > 0) nextWholeSecond() else this
        DurationUnit.MILLISECONDS -> roundedUpToNearestUnchecked(1.milliseconds)
        DurationUnit.MICROSECONDS -> roundedUpToNearestUnchecked(1.microseconds)
        DurationUnit.NANOSECONDS -> this
        else -> this
    }
}


/**
 * Returns this date-time, rounded down to match the precision of a given [unit].
 *
 * This is equivalent to [truncatedTo].
 */
fun LocalDateTime.roundedDownTo(unit: DurationUnit): LocalDateTime {
    return when (unit) {
        DurationUnit.DAYS -> date.atStartOfDay()
        DurationUnit.HOURS -> previousWholeHour()
        DurationUnit.MINUTES -> previousWholeMinute()
        DurationUnit.SECONDS -> previousWholeSecond()
        DurationUnit.MILLISECONDS -> previousWholeMillisecond()
        DurationUnit.MICROSECONDS -> previousWholeMicrosecond()
        DurationUnit.NANOSECONDS -> this
        else -> this
    }
}

/**
 * Returns this date-time, rounded down to the nearest hour that satisfies the [increment].
 *
 * The [increment] must multiply evenly into a 24-hour day.
 */
fun LocalDateTime.roundedDownToNearest(increment: Duration): LocalDateTime {
    return copyIfChanged(time = time.roundedDownToNearest(increment))
}


/**
 * Combines a local date and time with a UTC offset to create an [OffsetDateTime].
 */
infix fun LocalDateTime.at(offset: UtcOffset) = toInstant(offset).toSystemDefaultLocalDateTime()

infix fun LocalDateTime.at(offset: TimeZone) = toInstant(offset).toSystemDefaultLocalDateTime()

infix fun LocalTime.at(offset: UtcOffset) = this + offset.toDuration().toLocalTime()


/**
 * Combines a local date with a time and UTC offset to create an [OffsetDateTime].
 */
//infix fun LocalDate.at(offsetTime: UtcOffset) = this.toInstant(offset).toSystemDefaultLocalDateTime()

enum class Rounding {
    NEAREST,
    UP,
    NONE
}

/**
 * Date and time with a rounded minute
 * This returns a date with the seconds rounded and added to the minute
 * @param localDateTime the date and time
 * @return the date and time with 0 seconds and minutes including rounded seconds
 */
fun LocalDateTime.roundedMinute( rounding: Rounding = Rounding.NEAREST): LocalDateTime {
    val localDateTime = this
    val originalMinute = localDateTime.minute
    val (minute, second) = when (rounding) {
        Rounding.NEAREST -> originalMinute + (localDateTime.second / 60f).roundToInt() to 0
        Rounding.UP -> originalMinute + ceil(localDateTime.second / 60f).roundToInt() to 0
        Rounding.NONE -> originalMinute to localDateTime.second
    }

    val localDateTimeWithOldMinutes = LocalDateTime(
        year = localDateTime.year,
        monthNumber = localDateTime.monthNumber,
        dayOfMonth = localDateTime.dayOfMonth,
        hour = localDateTime.hour,
        minute = originalMinute,
        second = second
    )

    return if (originalMinute != minute) {
        val delta = minute - originalMinute
        add(localDateTimeWithOldMinutes, delta, DateTimeUnit.MINUTE)
    } else {
        localDateTimeWithOldMinutes
    }
}


/**
 * Add the specified amount of a unit of time to a particular date
 * @param localDateTime the original date
 * @param amount the amount to add
 * @param dateTimeUnit the field to add it to
 * @return the date with the offset added
 */
internal fun add(localDateTime: LocalDateTime, amount: Int, dateTimeUnit: DateTimeUnit): LocalDateTime {
    val timezone = TimeZone.UTC
    val instant = localDateTime.toInstant(timezone)
    return add(instant, amount, dateTimeUnit)
}

/**
 * Add the specified amount of a unit of time to a particular date
 * @param instant the gmt instant
 * @param amount the amount to add
 * @param dateTimeUnit the field to add it to
 * @return the date with the offset added
 */
internal fun add(instant: Instant, amount: Int, dateTimeUnit: DateTimeUnit): LocalDateTime {
    val timezone = TimeZone.UTC
    val updatedInstant = instant.plus(amount, dateTimeUnit, timezone)
    return updatedInstant.toLocalDateTime(timezone)
}

infix fun LocalDateTime.durationUntil(end: LocalDateTime): Duration {
    val timeZone = TimeZone.currentSystemDefault()

    return end.toInstant(timeZone) - this.toInstant(timeZone)
}

fun LocalDateTime.nextMatch(
    atSeconds: IntRange = 0..59,
    atMinutes: IntRange = 0..59,
    atHours: IntRange = 0..23,
    onDaysOfMonth: IntRange = 1..31,
    inMonths: ClosedRange<Month> = Month.JANUARY..Month.DECEMBER,
): LocalDateTime {
    // Ensure that the nextMatch of this LocalDateTime doesn't produce itself. If it does then increment the
    // nanosecond component by one to ensure that we produce a match that's distinct from this value.
    val time = if (matches(atSeconds, atMinutes, atHours, onDaysOfMonth, inMonths)) copy(nanosecond = 1) else this
    return time.nextMonth(inMonths)
        .nextDay(onDaysOfMonth, inMonths)
        .nextHour(atHours, onDaysOfMonth, inMonths)
        .nextMinute(atMinutes, atHours, onDaysOfMonth, inMonths)
        .nextSecond(atSeconds, atMinutes, atHours, onDaysOfMonth, inMonths)
}

private fun LocalDateTime.matches(
    atSeconds: IntRange = 0..59,
    atMinutes: IntRange = 0..59,
    atHours: IntRange = 0..23,
    onDaysOfMonth: IntRange = 1..31,
    inMonths: ClosedRange<Month> = Month.JANUARY..Month.DECEMBER,
): Boolean {
    return nanosecond == 0 && second in atSeconds && minute in atMinutes && hour in atHours
            && dayOfMonth in onDaysOfMonth && month in inMonths
}

private fun LocalDateTime.nextMonth(
    inMonths: ClosedRange<Month>,
    increment: Boolean = false,
): LocalDateTime {
    val incrementedMonth = if (increment) month.inc() else month
    return when {
        incrementedMonth < month -> copy(year = year + 1, month = incrementedMonth.number)
        incrementedMonth in inMonths -> copy(month = incrementedMonth.number)
        incrementedMonth < inMonths.start -> copy(
            month= inMonths.start.number,
            dayOfMonth = 1,
            hour = 0,
            minute = 0,
            second = 0,
            nanosecond = 0
        )
        incrementedMonth > inMonths.endInclusive -> copy(
            year = year + 1,
            month = inMonths.start.number,
            dayOfMonth = 1,
            hour = 0,
            minute = 0,
            second = 0,
            nanosecond = 0
        )
        else -> error("This should be impossible")
    }
}

@OptIn(ExperimentalTime::class)
private fun LocalDateTime.nextDay(
    onDaysOfMonth: IntRange,
    inMonths: ClosedRange<Month>,
    increment: Boolean = false,
): LocalDateTime {
    require(onDaysOfMonth.first >= 1 && onDaysOfMonth.last <= 31) { "onDaysOfMonth $onDaysOfMonth not in range 1..31." }
    val incrementedDay = when {
        increment && dayOfMonth + 1 <= month.daysIn(year) -> dayOfMonth + 1
        increment -> 1
        else -> dayOfMonth
    }
    return when {
        incrementedDay < dayOfMonth -> copy(dayOfMonth = incrementedDay).nextMonth(inMonths, increment = true)
        incrementedDay in onDaysOfMonth -> copy(dayOfMonth = incrementedDay)
        incrementedDay < onDaysOfMonth.first -> {
            if (onDaysOfMonth.first <= month.daysIn(year)) {
                copy(dayOfMonth = onDaysOfMonth.first)
            } else {
                nextMonth(inMonths, increment = true).copy(dayOfMonth = 1)
            }
        }
        incrementedDay > onDaysOfMonth.last -> {
            nextMonth(inMonths, increment = true).copy(dayOfMonth = 1)
        }
        else -> error("This should be impossible.")
    }
}

private fun LocalDateTime.nextHour(
    atHours: IntRange,
    onDaysOfMonth: IntRange,
    inMonths: ClosedRange<Month>,
    increment: Boolean = false,
): LocalDateTime {
    require(atHours.first >= 0 && atHours.last <= 23) { "atHours $atHours not in range 0..23." }
    val incrementedHour = if (increment) (hour + 1) % 24 else hour
    return when {
        incrementedHour < hour -> nextDay(onDaysOfMonth, inMonths, increment = true).copy(hour = incrementedHour)
        incrementedHour in atHours -> copy(hour = incrementedHour)
        incrementedHour < atHours.first -> copy(hour = atHours.first, minute = 0, second = 0, nanosecond = 0)
        incrementedHour > atHours.last -> nextDay(onDaysOfMonth, inMonths, increment = true).copy(
            hour = atHours.first,
            minute = 0,
            second = 0,
            nanosecond = 0,
        )
        else -> error("This should be impossible.")
    }
}

private fun LocalDateTime.nextMinute(
    atMinutes: IntRange,
    atHours: IntRange,
    onDaysOfMonth: IntRange,
    inMonths: ClosedRange<Month>,
    increment: Boolean = false,
): LocalDateTime {
    require(atMinutes.first >= 0 && atMinutes.last <= 59) { "atMinutes $atMinutes not in range 0..59." }
    val incrementedMinute = if (increment) (minute + 1) % 60 else minute
    return when {
        incrementedMinute < minute -> nextHour(atHours, onDaysOfMonth, inMonths, increment = true).copy(
            minute = incrementedMinute
        )
        incrementedMinute in atMinutes -> copy(minute = incrementedMinute)
        incrementedMinute < atMinutes.first -> copy(minute = atMinutes.first, second = 0, nanosecond = 0)
        incrementedMinute > atMinutes.last -> nextHour(atHours, onDaysOfMonth, inMonths, increment = true).copy(
            minute = atMinutes.first,
            second = 0,
            nanosecond = 0,
        )
        else -> error("This should be impossible.")
    }
}

private fun LocalDateTime.nextSecond(
    atSeconds: IntRange,
    atMinutes: IntRange,
    atHours: IntRange,
    onDaysOfMonth: IntRange,
    inMonths: ClosedRange<Month>,
): LocalDateTime {
    require(atSeconds.first >= 0 && atSeconds.last <= 59) { " atSeconds $atSeconds not in range 0..59." }
    val incrementedSecond = if (nanosecond > 0) (second + 1) % 60 else second
    return when {
        incrementedSecond < second -> nextMinute(atMinutes, atHours, onDaysOfMonth, inMonths, increment = true).copy(
            second = incrementedSecond
        )
        incrementedSecond in atSeconds -> copy(second = incrementedSecond)
        incrementedSecond < atSeconds.first -> copy(second = atSeconds.first)
        incrementedSecond > atSeconds.last -> nextMinute(
            atMinutes,
            atHours,
            onDaysOfMonth,
            inMonths,
            increment = true,
        ).copy(second = atSeconds.first)
        else -> error("This should be impossible.")
    }.copy(nanosecond = 0)
}

fun LocalDateTime.getSecondsInDegrees(floatPrecision: Boolean) = time.getSecondsInDegrees(floatPrecision)

fun LocalDateTime.getMinutesInDegrees() = time.getMinutesInDegrees()

fun LocalDateTime.getHoursInDegrees() = time.getHoursInDegrees()

fun LocalDateTime.getHoursInDegrees24() = time.getHoursInDegrees24()


fun LocalDateTime.isMonday() = dayOfWeek == DayOfWeek.MONDAY
fun LocalDateTime.isTuesday() = dayOfWeek == DayOfWeek.TUESDAY
fun LocalDateTime.isWednesday() = dayOfWeek == DayOfWeek.WEDNESDAY
fun LocalDateTime.isThursday() = dayOfWeek == DayOfWeek.THURSDAY
fun LocalDateTime.isFriday() = dayOfWeek == DayOfWeek.FRIDAY
fun LocalDateTime.isSaturday() = dayOfWeek == DayOfWeek.SATURDAY
fun LocalDateTime.isSunday() = dayOfWeek == DayOfWeek.SUNDAY

fun LocalDateTime.isWeekend() = isSaturday() or isSunday()
fun LocalDateTime.isWeekday() = !isWeekend()

@OptIn(ExperimentalTime::class)
fun T.startOfThisWeekAdjusted(
    startDayOfWeek: DayOfWeek,
    secondOfDay: Int,
): LocalDateTime {
    val dateTime =
        currentDateTime.apply {
            if (time.toSecondOfDay() < secondOfDay) {
                minus(DatePeriod(days = 1))
            }
        }
    val timeZone = TimeZone.currentSystemDefault()
    var date = dateTime.date
    while (date.dayOfWeek != startDayOfWeek) {
        date = date.minus(1, DateTimeUnit.DAY)
    }

    val startOfWeekInstant = date.atStartOfDayIn(timeZone).plus(secondOfDay.seconds)
    return startOfWeekInstant.toLocalDateTime(timeZone)
}

@OptIn(ExperimentalTime::class)
fun T.startOfThisMonth(secondOfDay: Int): LocalDateTime {
    val dateTime =
        currentDateTime.apply {
            if (time.toSecondOfDay() < secondOfDay) {
                minus(DatePeriod(days = 1))
            }
        }
    val date =
        LocalDate(
            dateTime.date.year,
            dateTime.date.month,
            1,
        )
    val startOfMonthInstant = date.atStartOfDayIn(TimeZone.currentSystemDefault()).plus(secondOfDay.seconds)
    return startOfMonthInstant.toLocalDateTime(TimeZone.currentSystemDefault())
}
